Testing python interface for Thorlabs APT¶

awojdyla@lbl.gov, Dec 2025

In [250]:
import pylablib
In [270]:
# stage = Thorlabs.KinesisMotor("27000001")  # connect to the stage
# stage.home()  # home the stage
# stage.wait_for_home()  # wait until homing is done
# stage.move_by(1000)  # move by 1000 steps
# stage.wait_move()  # wait until moving is done
# stage.jog("+")  # initiate jog (continuous move) in the positive direction
# time.sleep(1.)  # wait for 1 second
# stage.stop()  # stop the motion
# stage.close()
In [251]:
from pylablib.devices import Thorlabs

# List available APT devices
devices = Thorlabs.list_kinesis_devices()
print("Available devices:", devices)
Available devices: [('83812453', 'APT DC Motor Controller'), ('83829778', 'APT DC Motor Controller'), ('83813170', 'APT DC Motor Controller'), ('83815134', 'APT DC Motor Controller')]
In [252]:
# Connect to the motor (replace with your device serial number)
motor_x = Thorlabs.KinesisMotor('83829778')
motor_y = Thorlabs.KinesisMotor('83815134')
motor_z = Thorlabs.KinesisMotor('83812453')
In [121]:
motor_x.move_by(20000)
In [186]:
motor_y.move_by(-30000)
In [295]:
motor_z.move_by(-5000000)  

connect to camera¶

In [223]:
from pypylon import pylon

tl_factory = pylon.TlFactory.GetInstance()

# Create a device info object and set the IP address
device_info = pylon.DeviceInfo()
device_info.SetDeviceClass("BaslerGigE")  # Ensure we target GigE cameras
device_info.SetIpAddress("192.168.10.212")

# Try to create the camera object
camera = pylon.InstantCamera(tl_factory.CreateDevice(device_info))
In [224]:
camera.Open()
print(f"Connected to camera: {camera.GetDeviceInfo().GetModelName()}")
Connected to camera: a2A1920-51gcBAS
In [225]:
# Acquire one image from the camera
# Set exposure time to minimum (19 microseconds)
camera.ExposureTime.SetValue(19)
print(f"Exposure time set to: {camera.ExposureTime.GetValue()} µs")
camera.StartGrabbingMax(1)
grab_result = camera.RetrieveResult(5000, pylon.TimeoutHandling_ThrowException)

if grab_result.GrabSucceeded():
    image = grab_result.Array
    print(f"Image acquired successfully. Shape: {image.shape}")
else:
    print("Image grab failed")

grab_result.Release()
camera.StopGrabbing()
Exposure time set to: 19.0 µs
Image acquired successfully. Shape: (1200, 1920)
In [226]:
# # Determine pixel size automatically for known cameras (Basler a2A1920-51gc with Sony IMX392)
# # and fall back to probing the camera node map or asking the user.
# from matplotlib import pyplot as plt
# import numpy as np

# def _try_get_node_value(nodemap, candidates):
#     for name in candidates:
#         try:
#             node = nodemap.GetNode(name)
#             if node is not None:
#                 try:
#                     val = node.GetValue()
#                 except Exception:
#                     # some nodes may need ToString()
#                     try:
#                         val = float(node.ToString())
#                     except Exception:
#                         val = None
#                 if val is not None:
#                     return float(val)
#         except Exception:
#             continue
#     return None

# # Known camera -> pixel size (µm)
# model = camera.GetDeviceInfo().GetModelName()
# pixel_size_um = None
# if any(k in model for k in ("a2A1920-51gc", "a2A1920-51", "IMX392")):
#     # Sony IMX392 pixel pitch
#     pixel_size_um = 3.45

# if pixel_size_um is None:
#     try:
#         nodemap = camera.GetNodeMap()
#         pixel_candidates = ["PixelSize", "PixelSizeX", "PixelSizeUm", "PixelPitch"]
#         pixel_size_um = _try_get_node_value(nodemap, pixel_candidates)
#         # try sensor width -> compute pixel pitch
#         if pixel_size_um is None:
#             sensor_candidates = ["SensorWidth", "SensorWidth_mm", "SensorWidth_um"]
#             sensor_width = _try_get_node_value(nodemap, sensor_candidates)
#             if sensor_width is not None:
#                 # guess units: if < 10 assume mm else µm
#                 if sensor_width < 10:
#                     sensor_width_mm = sensor_width
#                 else:
#                     sensor_width_mm = sensor_width / 1000.0
#                 pixel_size_um = (sensor_width_mm * 1000.0) / image.shape[1]
#     except Exception:
#         pixel_size_um = None

# if pixel_size_um is None:
#     # prompt the user as last resort
#     try:
#         pixel_size_um = float(input("Pixel size (µm) not found. Enter pixel size in micrometers: ").strip())
#     except Exception:
#         raise RuntimeError("Pixel size not available from camera; please provide it manually.")

# print(f"Camera model: {model}")
# print(f"Using pixel size: {pixel_size_um:.3f} µm (X/Y assumed equal)")

# # store for downstream cells
# camera_pixel_size_um = pixel_size_um

# # Plot image with physical axes and optional scale bar
# from matplotlib import pyplot as plt
# import numpy as np

# def add_scalebar(ax, length_um, pixel_size_um, location='lower right', color='white', fontsize=10, pad=0.1):
#     """Add a simple scalebar in axis coordinates."""
#     # convert length in µm to axis data units
#     length_data = length_um
#     xlim = ax.get_xlim()
#     ylim = ax.get_ylim()
#     # place in axis coords
#     if location == 'lower right':
#         x = xlim[1] - (xlim[1] - xlim[0]) * 0.05 - length_data
#         y = ylim[0] + (ylim[1] - ylim[0]) * 0.05
#     elif location == 'lower left':
#         x = xlim[0] + (xlim[1] - xlim[0]) * 0.05
#         y = ylim[0] + (ylim[1] - ylim[0]) * 0.05
#     else:
#         x = xlim[0] + (xlim[1] - xlim[0]) * 0.05
#         y = ylim[0] + (ylim[1] - ylim[0]) * 0.05
#     ax.plot([x, x + length_data], [y, y], color=color, linewidth=3)
#     ax.text(x + length_data / 2, y + (ylim[1] - ylim[0]) * 0.02, f"{length_um:g} µm",
#             color=color, ha='center', va='bottom', fontsize=fontsize)

# # choose unit: 'um' or 'mm'
# unit = 'um'

# h, w = image.shape[:2]
# pixel_size_um = camera_pixel_size_um
# if unit == 'um':
#     x_max = w * pixel_size_um
#     y_max = h * pixel_size_um
#     xlabel = 'X (µm)'
#     ylabel = 'Y (µm)'
# elif unit == 'mm':
#     x_max = w * pixel_size_um / 1000.0
#     y_max = h * pixel_size_um / 1000.0
#     xlabel = 'X (mm)'
#     ylabel = 'Y (mm)'
# else:
#     raise ValueError("unit must be 'um' or 'mm'")

# extent = (0, x_max, 0, y_max)  # left, right, bottom, top in chosen unit
# plt.figure(figsize=(8,6))
# ax = plt.gca()
# if image.ndim == 2:
#     plt.imshow(image, cmap='gray', extent=extent, origin='lower')
# else:
#     plt.imshow(image, extent=extent, origin='lower')
# plt.xlabel(xlabel)
# plt.ylabel(ylabel)
# plt.title(f"{model} — pixel {pixel_size_um:.2f} µm")
# plt.tight_layout()

# # add a 100 µm scalebar by default (adjust as needed)
# try:
#     add_scalebar(ax, length_um=100, pixel_size_um=pixel_size_um, location='lower right', color='white')
# except Exception:
#     pass

# plt.show()
In [227]:
def acquire_and_plot_image(camera, exposure_time_us=19, unit='um', scalebar_length_um=100, figsize=(6,4), title=None):
    """
    Acquire one image from the camera and plot it with physical axes.
    
    Parameters:
    -----------
    camera : pypylon.pylon.InstantCamera
        The camera instance to acquire from
    exposure_time_us : float, optional
        Exposure time in microseconds (default: 19)
    unit : str, optional
        'um' or 'mm' for axis units (default: 'um')
    scalebar_length_um : float, optional
        Length of scalebar in micrometers (default: 100)
    figsize : tuple, optional
        Figure size (default: (6,4))
    title : str, optional
        Custom title for the plot. If None, uses default format (default: None)
    
    Returns:
    --------
    image : numpy.ndarray
        The acquired image
    pixel_size_um : float
        The pixel size in micrometers
    """
    import numpy as np
    from matplotlib import pyplot as plt
    
    # Set exposure and acquire
    camera.ExposureTime.SetValue(exposure_time_us)
    # print(f"Exposure time set to: {camera.ExposureTime.GetValue()} µs")
    camera.StartGrabbingMax(1)
    grab_result = camera.RetrieveResult(5000, pylon.TimeoutHandling_ThrowException)
    
    if grab_result.GrabSucceeded():
        image = grab_result.Array
        # print(f"Image acquired successfully. Shape: {image.shape}")
    else:
        print("Image grab failed")
        grab_result.Release()
        camera.StopGrabbing()
        return None, None
    
    grab_result.Release()
    camera.StopGrabbing()
    
    # Determine pixel size
    def _try_get_node_value(nodemap, candidates):
        for name in candidates:
            try:
                node = nodemap.GetNode(name)
                if node is not None:
                    try:
                        val = node.GetValue()
                    except Exception:
                        try:
                            val = float(node.ToString())
                        except Exception:
                            val = None
                    if val is not None:
                        return float(val)
            except Exception:
                continue
        return None
    
    model = camera.GetDeviceInfo().GetModelName()
    pixel_size_um = None
    
    if any(k in model for k in ("a2A1920-51gc", "a2A1920-51", "IMX392")):
        pixel_size_um = 3.45
    
    if pixel_size_um is None:
        try:
            nodemap = camera.GetNodeMap()
            pixel_size_um = _try_get_node_value(nodemap, ["PixelSize", "PixelSizeX", "PixelSizeUm", "PixelPitch"])
            if pixel_size_um is None:
                sensor_width = _try_get_node_value(nodemap, ["SensorWidth", "SensorWidth_mm", "SensorWidth_um"])
                if sensor_width is not None:
                    sensor_width_mm = sensor_width if sensor_width < 10 else sensor_width / 1000.0
                    pixel_size_um = (sensor_width_mm * 1000.0) / image.shape[1]
        except Exception:
            pass
    
    if pixel_size_um is None:
        try:
            pixel_size_um = float(input("Pixel size (µm) not found. Enter pixel size in micrometers: ").strip())
        except Exception:
            raise RuntimeError("Pixel size not available from camera; please provide it manually.")
    
    # print(f"Camera model: {model}")
    # print(f"Using pixel size: {pixel_size_um:.3f} µm (X/Y assumed equal)")
    
    # Plot with physical axes
    def add_scalebar(ax, length_um, pixel_size_um, location='lower right', color='white', fontsize=10):
        length_data = length_um
        xlim = ax.get_xlim()
        ylim = ax.get_ylim()
        if location == 'lower right':
            x = xlim[1] - (xlim[1] - xlim[0]) * 0.05 - length_data
            y = ylim[0] + (ylim[1] - ylim[0]) * 0.05
        else:
            x = xlim[0] + (xlim[1] - xlim[0]) * 0.05
            y = ylim[0] + (ylim[1] - ylim[0]) * 0.05
        ax.plot([x, x + length_data], [y, y], color=color, linewidth=3)
        ax.text(x + length_data / 2, y + (ylim[1] - ylim[0]) * 0.02, f"{length_um:g} µm",
                color=color, ha='center', va='bottom', fontsize=fontsize)
    
    h, w = image.shape[:2]
    if unit == 'um':
        x_max = w * pixel_size_um
        y_max = h * pixel_size_um
        xlabel, ylabel = 'X (µm)', 'Y (µm)'
    elif unit == 'mm':
        x_max = w * pixel_size_um / 1000.0
        y_max = h * pixel_size_um / 1000.0
        xlabel, ylabel = 'X (mm)', 'Y (mm)'
    else:
        raise ValueError("unit must be 'um' or 'mm'")
    
    extent = (0, x_max, 0, y_max)
    plt.figure(figsize=figsize)
    ax = plt.gca()
    plt.imshow(image, cmap='gray' if image.ndim == 2 else None, extent=extent, origin='lower')
    plt.xlabel(xlabel)
    plt.ylabel(ylabel)
    
    # Use custom title if provided, otherwise use default format
    if title is None:
        plt.title(f"{model} — pixel {pixel_size_um:.2f} µm")
    else:
        plt.title(title)
    
    plt.tight_layout()
    
    try:
        add_scalebar(ax, scalebar_length_um, pixel_size_um, color='white')
    except Exception:
        pass
    
    plt.show()
    
    return image, pixel_size_um

Zone plate 1 (row 3, col 11), changing z-distance

In [ ]:
# # Call with default parameters (19 µs exposure, 100 µm scalebar)
# image, pixel_size_um = acquire_and_plot_image(camera, title='Original position')

# motor_z.move_by(100)
# for i in range(1, 10):
#     image, pixel_size_um = acquire_and_plot_image(camera, title=f'After moving Z by +{i*100} steps')
#     motor_z.move_by(100)
In [356]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position')
No description has been provided for this image
In [357]:
motor_z.move_by(100000)
In [358]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 100000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [359]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 200000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [360]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 300000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [365]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 400000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [366]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 500000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [367]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 600000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [368]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 700000 steps')
No description has been provided for this image

Zone plate 2 (row 4, col 11) changing z-distance

In [ ]:
# # Call with default parameters (19 µs exposure, 100 µm scalebar)
# image, pixel_size_um = acquire_and_plot_image(camera, title='Original position')

# motor_z.move_by(100)
# for i in range(1, 10):
#     image, pixel_size_um = acquire_and_plot_image(camera, title=f'After moving Z by +{i*100} steps')
#     motor_z.move_by(100)
In [340]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position')
No description has been provided for this image
In [341]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 100000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [342]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 200000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [343]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 300000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [344]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 400000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [345]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 500000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [346]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 600000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [349]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 700000 steps')
motor_z.move_by(100000)
No description has been provided for this image

Zone plate 3 (row 2, col 11) changing z-distance

In [296]:
# # Call with default parameters (19 µs exposure, 100 µm scalebar)
# image, pixel_size_um = acquire_and_plot_image(camera, title='Original position')

# motor_z.move_by(100)
# for i in range(1, 10):
#     image, pixel_size_um = acquire_and_plot_image(camera, title=f'After moving Z by +{i*100} steps')
#     motor_z.move_by(100)
In [350]:
motor_z.move_by(-10000000)
In [327]:
# Call with default parameters (19 µs exposure, 100 µm scalebar)
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position')
motor_z.move_by(100000)
No description has been provided for this image
In [308]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 100000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [328]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 200000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [329]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 300000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [330]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 400000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [331]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 500000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [332]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 600000 steps')
motor_z.move_by(100000)
No description has been provided for this image
In [333]:
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position + 700000 steps')
motor_z.move_by(100000)
No description has been provided for this image

Zone plate 4 (row 1, col 11) changing z-distance

In [ ]:
# Call with default parameters (19 µs exposure, 100 µm scalebar)
image, pixel_size_um = acquire_and_plot_image(camera, title='Original position')
No description has been provided for this image
In [281]:
motor_z.move_by(100000)
In [ ]:
image, pixel_size_um = acquire_and_plot_image(camera, title=f'After moving Z by +100000 steps')
No description has been provided for this image
In [283]:
motor_z.move_by(100000)
In [284]:
image, pixel_size_um = acquire_and_plot_image(camera, title=f'After moving Z by +200000 steps')
No description has been provided for this image
In [285]:
motor_z.move_by(100000)
In [286]:
image, pixel_size_um = acquire_and_plot_image(camera, title=f'After moving Z by +300000 steps')
No description has been provided for this image
In [287]:
motor_z.move_by(100000)
In [288]:
image, pixel_size_um = acquire_and_plot_image(camera, title=f'After moving Z by +400000 steps')
No description has been provided for this image
In [289]:
motor_z.move_by(100000)
In [290]:
image, pixel_size_um = acquire_and_plot_image(camera, title=f'After moving Z by +500000 steps')
No description has been provided for this image
In [ ]:
motor_z.move_by(100000)
In [292]:
image, pixel_size_um = acquire_and_plot_image(camera, title=f'After moving Z by +600000 steps')
No description has been provided for this image
In [293]:
motor_z.move_by(100000)
In [294]:
image, pixel_size_um = acquire_and_plot_image(camera, title=f'After moving Z by +700000 steps')
No description has been provided for this image
In [ ]:
# x and y seem to be flipped, but i don't know how to change it (without changing the labels on the motors)
In [ ]:
# don't know how to relate steps to metric units (pylablib documentation says this is usually impossible?)
# for all the z-distances, the "original position" is at the farthest possible distance away from the pinhole

changing z-distance

In [ ]:
 
In [249]:
motor_x.close()
motor_y.close()
motor_z.close()
In [ ]:
camera.Close()